using System;
using System.Collections;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Linq;
using System.Runtime.Serialization;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Web;
using Dimok;
using mshtml;
using SHDocVw;
using IE = Dimok.IE;
using System.Collections.Generic;



namespace Manheim {
	public enum PriceType { Above, Average, Below };
	
	public class ManheimClient{
		public static ManheimServer Activate(){	return (ManheimServer)Activator.GetObject(typeof(ManheimServer),Dimok.Config.Manheim.TcpPath);	}
	}
	[Serializable]
	public class PrintableVersionNotFound : Dimok.Exceptions.RemoteException {
		public PrintableVersionNotFound():base(){}
		public PrintableVersionNotFound(SerializationInfo info, StreamingContext context):base(info,context) {}
	}

	public class ManheimServer : MarshalByRefObject{
		public static IManheimForm Form;
		public static object locker = new object();
		// INs
		private string region;
		private string vin;
		private int mileage;
		private string makeModelStyle;
		private string[] makes = new string[] { };
		private string[] models = new string[] { };
		private string[] bodies = new string[] { };
		private ManheimPriceDS.PriceDataTable dtPrice = new ManheimPriceDS().Price;
		// OUTs
		private int price = 0;
		private string description = "";

		private Exception exception;

		//private static Dimok.IE.IEHolder ieh = new Dimok.IE.IEHolder();
		//private static Dimok.IE.IE ie{get{return ieh.ie;}}
		private static Dimok.IE.IE ie { get { return Form.IEH.ie; } }

		#region IEVersion
		static double _IEVersion = -1;
		static double IEVersion {
			get {
				if (_IEVersion >= 0) return _IEVersion;
				using (var wie = new WatiN.Core.IE(ie.ie)) {
					wie.AutoClose = false;
					var version = wie.Eval("navigator.appVersion");
					double.TryParse(Regex.Match(version, "MSIE ([0-9.]+)").Groups[1].Value, out _IEVersion);
					return _IEVersion;
				}
			}
		}
		#endregion

		public string[] GetMakes(string Year) {
			Thread t = new Thread(() => { GetSelect_(Year, "", "", ""); });
			t.SetApartmentState(ApartmentState.STA);
			t.Start(); t.Join(30 * 1000);
			if (exception != null) throw new Exception(vin, exception);
			return makes;
		}
		public string[] GetModels(string Year, string Make) {
			Thread t = new Thread(() => {  GetSelect_(Year, Make, "", "");  });
			t.SetApartmentState(ApartmentState.STA);
			t.Start(); t.Join(30 * 1000);
			if (exception != null) throw new Exception(vin, exception);
			return models;
		}
		public string[] GetStyles(string Year, string Make,string Model) {
			Thread t = new Thread(() => {  GetSelect_(Year, Make, Model, ""); } );
			t.SetApartmentState(ApartmentState.STA);
			t.Start(); t.Join(30*1000);
			if (exception != null) throw new Exception(vin, exception);
			return bodies;
		}
		public ManheimPriceDS GetPrice(string Year, string Make, string Model, string Style, int mileage) {
			this.mileage = mileage;
			Thread t = new Thread(() => { GetSelect_(Year, Make, Model, Style); });
			t.SetApartmentState(ApartmentState.STA);
			t.Start(); t.Join(30 * 1000);
			if (exception != null) throw new Exception(vin, exception);
			return dtPrice.DataSet as ManheimPriceDS;
		}
		[STAThread]
		[System.Runtime.CompilerServices.MethodImpl( System.Runtime.CompilerServices.MethodImplOptions.Synchronized)]
		public void GetSelect_(string Year,string Make,string Model,string Style) {
			lock (locker) {

				HTMLDocument doc;
				#region Login
				try {
					if (!ie.doc.title.StartsWith("Manheim.com | Internet MMR |")) Login();
					doc = ie.Document(0, 0);
				} catch (InvalidCastException) {
					Form.InitIE();
					LogIn(ie, region);
					doc = ie.Document(0, 0);
				} catch (System.Runtime.InteropServices.InvalidComObjectException) {
					Form.InitIE();
					LogIn(ie, region);
					doc = ie.Document(0, 0);
				}
				if (doc == null) LogIn(ie, region);
				#endregion
				HTMLSelectElement selYear = IE.IE.Tags(doc.body, "select").OfType<HTMLSelectElement>()
					.SingleOrDefault(s => s.name == "collectionSelectYear");
				if (selYear == null) {
					exception = new Dimok.Exceptions.RemoteException("Year select box not found.");
					return;
				}
				if (selYear.value != Year) {
					selYear.value = Year;
					selYear.form.submit();
					ie.WaitIE(false);
				}
				IHTMLSelectElement selMake = IE.IE.Tags(doc.body, "select").OfType<IHTMLSelectElement>()
					.SingleOrDefault(s => s.name == "collectionSelectMake");
				if (selMake == null) {
					exception = new Dimok.Exceptions.RemoteException("Make select box not found.");
					return;
				}
				if (Make == "") {
					makes = selMake.Cast<HTMLOptionElement>().Select(o => o.text).Where(t => !t.StartsWith("Select ")).ToArray();
					return;
				}
				var make = selMake.Cast<HTMLOptionElement>().SingleOrDefault(o => o.text == Make);
				if (make == null) {
					exception = new Dimok.Exceptions.RemoteException("Make " + Make + " not found.");
					return;
				}
				if (!make.selected) {
					make.selected = true;
					selMake.form.submit();
					ie.WaitIE(false);
				}
				IHTMLSelectElement selModel = IE.IE.Tags(doc.body, "select").OfType<IHTMLSelectElement>()
					.SingleOrDefault(s => s.name == "collectionSelectModel");
				if (Model == "") {
					models = selModel.Cast<HTMLOptionElement>().Select(o => o.text).Where(t => !t.StartsWith("Select ")).ToArray();
					return;
				}
				var model = selModel.Cast<HTMLOptionElement>().SingleOrDefault(o => o.text == Model);
				if (model == null) {
					exception = new Dimok.Exceptions.RemoteException("Model " + Model + " not found.");
					return;
				}
				if (!model.selected) {
					model.selected = true;
					selModel.form.submit();
					ie.WaitIE(false);
				}
				IHTMLSelectElement selBodies = IE.IE.Tags(doc.body, "select").OfType<IHTMLSelectElement>()
					.SingleOrDefault(s => s.name == "collectionSelectBody");
				if (Style == "") {
					bodies = selBodies.Cast<HTMLOptionElement>().Select(o => o.text).Where(t => !t.StartsWith("Select ")).ToArray();
					return;
				}
				var style = selBodies.Cast<HTMLOptionElement>().SingleOrDefault(o => o.text == Style);
				if (style == null) {
					exception = new Dimok.Exceptions.RemoteException("Style " + Style + " not found.");
					return;
				}
				style.selected = true;
				selBodies.form.submit();
				ie.WaitIE(false);
				//			try {
				GetPrices();
				//} catch (UnauthorizedAccessException exc) {
				//  exception = exc;
				//  ie.Init();
				//}catch(Exception exc){
				//  exception = exc;
				//}
			}
		}

		static Exception GetExceptions(Exception exc) {
			var messages = new List<string>(new[]{exc.Message+Environment.NewLine+exc.StackTrace});
			while ((exc = exc.InnerException) != null) {
				messages.Add(exc.Message+Environment.NewLine+exc.StackTrace);
			}
			return new Exception(string.Join(
					Environment.NewLine + "***********************************************************" + Environment.NewLine,
					messages.ToArray()
				));
		}
		public ManheimPriceDS GetPriceByVIN(string region,string vin,int mileage,string makeModelStyle){
			if( (vin+"").Length < 9 )throw new Exception("Incorrect VIN["+vin+"]");
			if( vin.Length == 9 )vin = vin.Substring(0,8)+"|"+vin.Substring(8);
			this.vin = vin.Substring(0,10);
			this.mileage = mileage;	this.makeModelStyle = makeModelStyle;
			this.region = region;
			this.exception = null;
			this.price = 0;
			this.dtPrice.Clear();
			ie.PopUp = null;
			Thread t = new Thread(() => { GetPriceByVIN_(); });
			t.SetApartmentState(ApartmentState.STA);
			t.Start(); t.Join();
			if( exception != null )throw new Exception(vin,GetExceptions(exception));
			return dtPrice.DataSet as ManheimPriceDS;
		}	
		[STAThread]
		[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.Synchronized)]
		private void GetPriceByVIN_() {
			lock (locker) {
				try {
					//Dimok.IE.IE ie = new Dimok.IE.IE(Dimok.Config.Manheim.LogInURL);
					string status = "VIN:" + vin + ",Car:" + makeModelStyle + ",Miles:" + mileage;
					Form.Status = "Fetching " + status;
					HTMLDocument doc;

					#region Login
					try {
						if (DoNeedLogin()) Login();
						doc = ie.Document(0, 0);
					} catch (InvalidCastException) {
						Form.InitIE();
						LogIn(ie, region);
						doc = ie.Document(0, 0);
					}
					if (doc == null) Login();
					#endregion

					#region Navigate
					HTMLInputElement el = (HTMLInputElement)IE.IE.GetElementByName(doc, "vehicleVinProperty");
					IE.IE.SetElementValue(el as IHTMLElement , vin);
					IEnumerator en = el.form.GetEnumerator();
					while (en.MoveNext())
						if (((HTMLInputElement)en.Current).name == "beanCollectionSelectRegion")
							((HTMLInputElement)en.Current).value = region;
					el.form.submit();
					ie.WaitIE(false);

					if (ie.Document(0).body.innerHTML.IndexOf("No records were returned") >= 0) {
						((HTMLAnchorElement)ie.Document(0).links.item(0, 0)).click();
						ie.WaitIE(false);
						return;
					}
					doc = ie.Document(0, 1, 1);
					if (doc == null) {
						SelectByMakeModelStyle(ie, region);
						//SetRegion(ie,region);
					}
					#endregion
					try {
						GetPrices();
					} catch (UnauthorizedAccessException exc) {
						exception = exc;
						ie.Init();
					}
					Form.Status = "Done with " + status;
				} catch (Exception exc) { exception = exc; } finally {
					//ie.Destroy();
				}
			}
		}

		private static bool DoNeedLogin() {
			return ie.doc.title.Contains("Login");
		}
		int ParseInt(string text) {
			var i = 0;
			int.TryParse(Regex.Replace(text, "[\\D]", ""),out i);
			return i;
		}
		private void GetPrices(){
			HTMLDocument doc = ie.Document(0, 0);

			#region Description
				description  = GetDescription(doc,"vehicleinformationyear","collectionSelectYear");
				description += ","+GetDescription(doc,"vehicleinformationmake","collectionSelectMake");
				description += ","+GetDescription(doc,"vehicleinformationmodel","collectionSelectModel");
				description += ","+GetDescription(doc,"vehicleinformationbody","collectionSelectBody");
				#endregion
		
				doc = ie.Document(0,1,1);
				dtPrice.Clear();

				#region Milage
				if ( mileage > 0 ){
					IHTMLInputTextElement txtMileage = (IHTMLInputTextElement)IE.IE.GetElementByName(doc,"vehicleMileageProperty");
					IE.IE.SetElementValue((IHTMLElement)txtMileage,mileage);
					txtMileage.form.submit();
					ie.WaitIE(false);
					doc = ie.Document(0,1,1);
				}
				#endregion

				string textPrintable = GetPrintableVersion(false);
				Dictionary<string,int> odometers = new Dictionary<string,int>();
				using (var wie = new WatiN.Core.IE(ie.ie)) {
					wie.AutoClose = false;
					var frame = GetPricesFrame(wie);
					var odometerRow = frame.TableRow(WatiN.Core.Find.ByTextInColumn(new Regex("^Odometer"), 0));
					if (!odometerRow.Exists) throw new Exception("Odometer row not found.");
					var priceTypeRow = frame.TableRow(WatiN.Core.Find.ByTextInColumn(new Regex("^Above"), 1));
					if (!priceTypeRow.Exists) throw new Exception("PriceType row not found.");
					foreach (var cell in odometerRow.TableCells.Skip(1).Take(3))
						odometers.Add(priceTypeRow.TableCells[cell.Index].Text.Trim(), ParseInt(cell.Text.Trim()));
				}

				IHTMLElement tdPrices = IE.IE.GetElementByText(doc, "TD", "^Prices are based");
				HTMLTable table = (HTMLTable)((HTMLTable)IE.IE.FindParentByTag(tdPrices, "TABLE", false)).nextSibling;
				foreach (string priceType in Enum.GetNames(typeof(PriceType))) {
					IHTMLElement td = (IHTMLElement)IE.IE.GetElementByText((IHTMLElement)table, "TD", "^" + priceType + "$");
					int cellIndex = ((IHTMLTableCell)td).cellIndex;
					IHTMLElement row = td.parentElement;
					int rowIndex = ((IHTMLTableRow)row).rowIndex;
					row = (IHTMLElement)((IHTMLTableSection)row.parentElement).rows.item(rowIndex + 1, 0);
					td = (IHTMLElement)((IHTMLTableRow)row).cells.item(cellIndex, 0);
					price = int.Parse(Regex.Replace(td.innerText, "[\\D]", ""));
					dtPrice.AddPriceRow(priceType, price, description,textPrintable,odometers[priceType]);
				}
				if (IEVersion >= 7) {
					var tabs = IE.IE.GetElementByAttr(ie.Document(0, 1, 0), "name", "navTabs") as HTMLMapElement;
					if (tabs != null) {
						(((tabs).areas as IHTMLAreasCollection).item(1, null) as HTMLAreaElement).click();
						ie.WaitIE();
						textPrintable += GetPrintableVersion(true);
						dtPrice[0].Info = textPrintable;
						tabs = IE.IE.GetElementByAttr(ie.Document(0, 1, 0), "name", "navTabs") as HTMLMapElement;
						(((tabs).areas as IHTMLAreasCollection).item(0, null) as HTMLAreaElement).click();
					}
				}
		}

		private static WatiN.Core.Frame GetPricesFrame(WatiN.Core.IE wie) {
			return wie.Frames[0].Frame("vehDataFrame").Frames[1];
		}
		private string GetPrintableVersion(bool RemoveStuff) {
			using (var wie = new WatiN.Core.IE(ie.ie)) {
				wie.AutoClose = false;
				var link = wie.DomContainer.Frames[0].Frames[0].Link(l => l.InnerHtml.Contains("Printable Version"));
				if (!link.Exists) return "";
				link.Click();
				InternetExplorer iePrint = null;
				if (false) {
					ie.GoToLinkByText(ie.Document(0, 0).body, "Printable Version", true);
					long ticks = DateTime.Now.AddMilliseconds(3000).Ticks;
					iePrint = IE.IE.GetBrowserWindowByTitle("printable");
					while (ticks > DateTime.Now.Ticks && iePrint == null) {
						IE.IE.DoEventsAndWait(100);
						iePrint = IE.IE.GetBrowserWindowByTitle("printable");
					}
				}
				var ies = WatiN.Core.IE.InternetExplorers();
				var printableRegEx = new Regex("printable", RegexOptions.IgnoreCase);
				foreach (var popup in ies)
					if (printableRegEx.IsMatch(popup.Title)) {
						iePrint = popup.InternetExplorer as InternetExplorer;
						break;
					}
				string textPrintable = "";
				if (iePrint != null) {
					//textPrintable = Dimok.Web.Utils.MIMEConverter.ConvertWebPageToMHTString(iePrint.LocationURL);
					var d = ((HTMLDocument)iePrint.Document).documentElement;
					//IE.IE.Tag(d, "HEAD").insertAdjacentHTML("afterBegin", "<BASE href='" + iePrint.LocationURL + "' />");
					if (RemoveStuff)
						try {
							IE.IE.RemoveChild(IE.IE.GetElementByText(d, "TD", "Vehicle Identification").parentElement.parentElement);
							IE.IE.RemoveChild(IE.IE.GetElementByAttr(d, "value", "PRINT"));
							IE.IE.RemoveChild(IE.IE.GetElementByAttr(d, "value", "CLOSE"));
						} catch (IE.HTMLElementNotFoundException) { }

					textPrintable = d.outerHTML; ;
					//Regex.Replace(d.outerHTML,"<HEAD[^>]*>","<HEAD>"+"<BASE href='" +
					//Regex.Replace(iePrint.LocationURL, "[^/]*$", "") + "' />");
					iePrint.Quit();
				} //else throw new PrintableVersionNotFound();
				return textPrintable;
			}
		}

		private string GetDescription(HTMLDocument doc,string formName,string selectName){
			IHTMLFormElement form = IE.IE.GetElementByName(doc,formName) as IHTMLFormElement;
			if( form == null )throw new Exception("No <FORM name='"+formName+"' /> found.");
			IHTMLSelectElement sel = form.item(selectName,null) as IHTMLSelectElement;
			if( sel == null )throw new Exception("No <SELECT name='"+selectName+"' /> found.");
			return ((IHTMLOptionElement)sel.item(sel.selectedIndex,null)).text;
		}
		private void SelectByMakeModelStyle(Dimok.IE.IE ie,string region){
			HTMLDocument doc = ie.Document(0);
			HTMLTableCell td = (HTMLTableCell)IE.IE.GetElementByText(doc,"TD","^Year$");
			int rowIndex = ((IHTMLTableRow)td.parentElement).rowIndex;
			IHTMLTableSection tBody = (IHTMLTableSection)td.parentElement.parentElement;
			StringList sl = new StringList();
			for( int r = rowIndex+1; r < tBody.rows.length; r++ )
				sl.Add(((IHTMLElement)tBody.rows.item(r,0)).innerText);
			string reText = Regex.Replace("("+makeModelStyle,"\\s+","|")+")+";
			int rate = 0,index = 0;
			for( int r=0; r < sl.Count; r++ ){
				string rowText = sl[r];
				MatchCollection mc = Regex.Matches(rowText,reText,RegexOptions.IgnoreCase);
				if( mc.Count > rate ){
					rate = mc.Count;
					index = r;
				}
			}
			IHTMLTableRow row = (IHTMLTableRow)tBody.rows.item(rowIndex+index+1,0);
			td = (HTMLTableCell)row.cells.item(0,0);
			IHTMLElementCollection all = (IHTMLElementCollection)td.all;
			IHTMLElementCollection links = (IHTMLElementCollection)all.tags("A");
			HTMLAnchorElement link = (HTMLAnchorElement)links.item(0,0);
			link.href += "&regionid="+region;
			link.click();
			ie.WaitIE(false);
		}
		public void Login() {
			using (var wie = new WatiN.Core.IE(ie.ie)) {
				wie.AutoClose = false;

				if (wie.Title == "") wie.GoTo(Dimok.Config.Manheim.LogInURL);
				if (!DoNeedLogin()) return;
				var prefix = "Manheim.";
				var userNameKey = prefix + "User";
				var PasswordKey = prefix + "Password";
				var userName = System.Configuration.ConfigurationManager.AppSettings[userNameKey];
				if (string.IsNullOrEmpty(userName)) throw new Exception(userNameKey + " key is nit found in .config");
				var password = System.Configuration.ConfigurationManager.AppSettings[PasswordKey];
				if (string.IsNullOrEmpty(password)) throw new Exception(PasswordKey + " key is nit found in .config");
				var userNameId = "user_username";
				var userBox = wie.TextField(userNameId);
				if (!userBox.Exists) throw new Exception(userNameId + " textbox is not found in login form." + Environment.NewLine + wie.Url);
				var passwordId = "user_password";
				var passwordBox = wie.TextField(passwordId);
				if (!passwordBox.Exists) throw new Exception(passwordBox + " textbox is not found in login form." + Environment.NewLine + wie.Url);
				userBox.Value = userName;
				passwordBox.Value = userName;
				var rememberChkId = "remember";
				var remenberChkBox = wie.CheckBox(rememberChkId);
				if (!remenberChkBox.Exists) throw new Exception(remenberChkBox + " textbox is not found in login form." + Environment.NewLine + wie.Url);
				var submitId = "user_submit";
				var submitBox = wie.Button(submitId);
				if (!submitBox.Exists) throw new Exception(submitId + " button is not found in login form." + Environment.NewLine + wie.Url);
				userBox.Value = userName;
				passwordBox.Value = password;
				remenberChkBox.Checked = true;
				submitBox.Click();
				wie.GoTo(Dimok.Config.Manheim.LogInURL);
			}
		}
		public void LogIn(Dimok.IE.IE ie, string region) {
			Form.Status = "Logging In ...";
			ie.Navigate(Dimok.Config.Manheim.LogInURL);
			//SetRegion(region);
			Form.Status = "Logged In";
		}
		private void SetRegion(Dimok.IE.IE ie, string region ){
			Login();
			HTMLDocument doc = ie.Document(0,0);
			HTMLFormElement form = (HTMLFormElement)IE.IE.GetElementByName(doc,"vehicleinformationregions");
			HTMLSelectElement select = (HTMLSelectElement)((IHTMLElementCollection)form.all).item("beanCollectionSelectRegion",0);
			if( select.value == region )return;
			IE.IE.SetElementValue((IHTMLElement)select,region);
			form.submit();
			ie.WaitIE(false);
		}
	}
}
